和大多数语言一样,C++提供了条件执行语句、重复执行相同代码的循环语句和用于中断当前控制流的跳转语句。本章将详细介绍C++所支持的这些语句。

5.1 简单语句

C++大多数语句以分号结束。

表达式末尾加上分号就变成了表达式语句:执行表达式并丢弃掉求值结果。

最简单的语句是空语句:空语句中只含有一个单独的分号,语法需要但是逻辑上不需要的时候可使用。

ival + 5;		//表达式语句
cout << ival;

;	//空语句
while(cin >> s && s != sought)
    ;	//空语句

//空语句一般是无害的
ival = v1 + v2;;	//正确,第二个分号表示一条多余的空语句
//空语句并非总是无害的
while(iter != svec.end());	//while循环主体是空语句
	++iter;					//不属于循环的一部分

空语句使用时应该加上注释,使读代码的人知道该语句是有意省略的

复合语句是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作块(block)。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。

如果语法上需要一条语句,而逻辑上需要多条语句,则应该使用复合语句。块不以分号结束。

空块是指内部没有任何语句的一对花括号,空块的作用等价于空语句。

5.2 语句作用域

可以在if、switch、while和for语句的控制结构内定义变量。定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量就超出作用范围。

5.3 条件语句

5.3.1 if语句

if语句的作用是:判断一个指定的条件是否为真,根据判断结果决定是否执行另外一条语句。if语句有两种形式:一种含有else分支,另外一种没有。

if (condition)
    statement
    
if (condition)
    statement
else
    statement2
    
if (condition)
    statement
else if(condition2)
    statement2

当if分支多于else分支时,else与if语句如何匹配,这个问题通常称作悬垂else。C++规定else与离它最近的尚未匹配的if匹配,消除了程序的二义性。

对if或else之后写上花括号,可以避免代码混乱不清,也便于维护。使用花括号可以控制else的匹配if。

5.3.2 switch语句

switch语句提供了一条便利的途径使得我们能够在若干固定选项中做出选择。

case关键字和它对应的值一起被称为case标签。case标签必须是整型常量表达式。任何两个case标签的值不能相同,否则会引发错误,另外,default也是一种特殊的case标签。

如果某个case标签匹配成功了,将从该标签开始往后顺序执行所以case分支,除非显示中断这一过程,否则直到switch结尾才停下来。大多数情况下,在下一个case标签之前应有一条break语句。有时候我们需要多个case共享同一组操作,就可以故意省略break语句。C++形式比较自由,case标签之后不一定非得换行。

如果没有一个case标签匹配switch表达式的值,程序将执行紧跟在default标签后的语句。

即使不准备在default标签下做任何工作,定义一个default标签也是有用的。其目的在于告诉程序的读者,我们已经考虑了默认的情况,只是目前什么也没做。

标签不应该单独出现,它后面必须跟上一条语句或者另外一个case标签。如果switch结构以一个空的default标签作为结束,则该default标签后面必须跟上一条空语句或一个空块。

switch内部的变量定义:C++不允许跨过变量的初始化语句直接跳转到该变量的作用域内的另一个位置。

case true:
	//变量作用域是switch之内
	string file_name;	//错误:控制流绕过了一个隐式初始化的变量
	int ival = 0;		//错误:控制了绕过了一个显示初始化的变量
	int jval;			//正确:因为jval没有初始化
	break;
case false:
	//正确:jval虽然在作用域内,但是它没有被初始化
	jval = next_num();	//正确:给jval赋一个值
	if (file_name.empty())	//file_name在作用域内,但是没有被初始化
		//...

如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。

如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面所有的case标签都在变量的作用域之外。

case true:
	{
        //正确:声明语句位于语句块内部
    	string file_name = get_file_name();
	}
	break;
case false:
	if(file_name.empty());	//错误:file_name不在作用域之内

5.4 迭代语句

5.4.1 while语句

只要条件为真,while语句就重复地执行循环体。while的条件部分可以是一个表达式或者是一个带初始化的变量声明,通常来说,应该由条件本身或者循环体设法改变表达式的值,否则循环无法终止。

while(condition)
    statement

5.4.2 传统的for语句

初始化只执行一次,条件表达式值为真则执行循环体。

for (init-statement; condition; expression)
    statement

5.6.3 范围for语句

C++11新标准引入范围for语句,配合auto可方便遍历容器或其他序列的所有元素,如果需要执行写操作,循环变量必须声明为引用类型。

for (declaration : expression)
    statement

5.4.4 do while语句

do while语句:先执行循环体后检查条件,无论条件如何,至少执行一次循环。

do while语句应该在括号包围起来的条件后面用一个分号表示语句结束。

因为对于do while来说先执行语句或块,后判断条件,所以不允许在条件部分定义变量。

do
    statement
while (condition);

5.5 跳转语句

5.5.1 break语句

break语句负责终止离它最近的while、do while、for或switch语句,并从这些语句后的第一条语句开始继续执行。

break语句只能出现在迭代语句或者switch语句内部(包括嵌套在此类循环里的语句或块的内部),break语句的作用范围仅限于最近的循环或者switch。

5.5.2 continue语句

continue语句终止最近的循环中的当前迭代并立即开始下一处迭代,continue语句只能出现在for、while和do while循环的内部,或者嵌套在此类循环里的语句或块的内部。

嵌套在循环中的continue语句仅作用于离它最近的循环,只有switch嵌套在循环内部中时才能使用continue。

对于while或者do while语句来说,继续判断条件的值;对于传统for循环来说,继续执行for语句头的expression;对于范围for语句来说,用序列的下一个元素初始化循环控制变量。

5.5.3 goto语句

goto语句的作用是从goto语句无条件跳转到同一函数内的另一条语句。goto语句的语法形式是:

goto label;

label是用于标识一条语句的标识符。带标签语句是一种特殊的语句,在它之前有一个标识符以及一个冒号。标签标识符独立于变量或其他标识符的名字,因此,标签标识符可以和程序中其他实体标识符使用同一个名字而不会相互干扰。goto语句和控制器转向的那条带标签的语句必须位于同一个函数之内。

和switch语句相似,goto语句也不能将程序的控制权从变量的作用域之外转移到作用域之内:

	//...
	goto end;
	int ix = 10;	//错误:goto语句绕过了一个带初始化的变量定义
end:
	//错误:此次的代码需要使用ix,但是goto语句绕过了它的定义
	ix = 42;

向后跳过一个已经执行过的定义是合法的,跳回到变量定义之前意味着系统将销毁该变量,然后重新创建它:

//向后跳过一个带初始化的变量定义是合法的
begin:
	int sz = get_size();
	if(sz <= 0){
        goto begin;
    }

不要在程序中使用goto语句,goto语句会让程序难以理解又难修改。

5.6 try语句块和异常处理

异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。

当程序某部分检测到一个无法处理的问题时,需要用的异常处理,此时检测出问题的部分应该发出某种信号以表明程序遇到了故障无法继续下去,而信号发出方无须知道故障在何处得到处理。一旦发出异常信号,检测出问题的部分也就完成了任务。

如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。

异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:

  • throw表达式,异常检测部分使用throw表达式来表示它遇到了无法处理的问题,我们说throw引发了异常
  • try语句块,异常处理部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句“处理”异常,所以它们常被称作异常处理代码
  • 一套异常类,用于在throw表达式和相关的catch子句直接传递异常的具体信息

5.6.1 throw表达式

程序的异常检测部分使用throw表达式引发一个异常。throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw表达式后面通常紧跟一个分号,从而构成一条表达式语句。

抛出异常将终止当前的函数,并把控制器转移到能处理该异常的代码。

5.6.2 try语句块

形式:

try{
    program-statements	//组成程序的正常逻辑
}catch(exception-declaration){	//异常声明
    handler-statements
}catch(exception-declaration){
    handler-statements
}//...

在复杂的系统中,程序在遇到抛出异常的代码前,其执行路径已经经过了多个try语句块。在异常被抛出时,首先搜索抛出该异常的函数(a),如果没有找到匹配的catch子句,终止该函数(a),并在调用该函数的函数(b)中继续寻找,如果最终还是没有找到匹配的catch子句,这个新的函数(b)也被终止,继续搜索调用它(b)的函数(c)。以此类推,沿着程序的执行路径逐层回退,直到找到catch子句为止。如果最终还是没有找到任何匹配的catch子句,程序转到名为terminate的标准函数。该函数与系统有关,一般情况下,执行该函数将导致程序非正常退出。

对于没有任何try语句块定义的异常,也按照类似的方式处理。

在异常发生期间正确执行了“清理”工作的程序被称作异常安全的代码,编写异常安全的代码非常困难。

5.6.3 标准异常

C++标准定义了一组类,用于报告标准库函数遇到的问题,这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中:

  • exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外的信息
  • stdexcept头文件定义了几种常用的异常类
  • new头文件定义了bad_alloc异常类
  • type_info头文件定义了bad_cast异常类

image-20200615233825079

标准库异常类只定义了几种运算:创建和拷贝异常类型的对象、为异常类型的对象赋值。

  1. exception、bad_alloc、bad_cast对象则只能以默认初始化的方式进行初始化,不允许为这些对象提供初始值。
  2. 其他异常类的行为恰好相反:应该使用string对象或者C风格字符串初始化这些类型的对象,不允许使用默认初始化的方式,当创建这些对象时,必须提供初始值,该初始值含有错误相关的信息。

异常类型只定义了一个名为what的成员函数,不接收参数,返回值是一个指向C风格字符串的const char*。该函数的目的是提供关于异常的一些文本信息。

what函数返回的C风格字符串的内容与异常对象的类型有关:

  • 异常类型有字符串初始值,则what返回该字符串
  • 对于其他无初始值的异常类型来说,what返回的内容由编译器决定